package com.alibaba.servlet.diytomcat;

import com.alibaba.annotation.Autowired;
import com.alibaba.annotation.Controller;
import com.alibaba.annotation.RequestMapping;
import com.alibaba.annotation.Service;
import com.alibaba.servlet.MethodHandler;
import org.apache.commons.lang.StringUtils;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class DiyTomcatDispatcherServlet extends HttpServlet {

    private final Set<HttpServletRequest> set
            = new HashSet<>();

    private String[] configFiles;

    public DiyTomcatDispatcherServlet(String... configFiles) {
        this.configFiles = configFiles;
    }

    /**
     * 接收处理请求
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        set.add(req);
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        /**
         * 根据req的uri找到对应的 method并执行，返回执行结果
         */
        Object invokeResult = invokeMethod(req, resp);
        //简单操作，把方法返回的数据以字符串的形式写出
        assert invokeResult != null;
        resp.getWriter().write(invokeResult.toString() +  " " + set.size());
    }

    /**
     * 加载springmvc.properties配置文件。定义IOC需要扫描的包
     */
    private final Properties properties = new Properties();

    /**
     * 缓存扫描到的类的全类名
     */

    private final List<String> classNames = new ArrayList<>();

    /**
     * IOC容器，String是name，value是实例对象。
     */
    private final Map<String, Object> ioc = new ConcurrentHashMap<>();


    /**
     * handleMapping ，存储MethodHandler,见{@link MethodHandler}
     * key: url, value:MethodHandler
     */
    private final Map<String, MethodHandler> methodHandlerMap = new HashMap<>();

    /**
     * 重写Servlet容器初始化时的工作
     *
     * @param config
     * @throws ServletException
     */
    @Override
    public void init(ServletConfig config) throws ServletException {
        //1. 加载配置文件  springmvc.properties
        for (String configFile : configFiles) {
            doLoadConfig(configFile);
        }
        //2. 扫描相关的类——扫描注解
        doScan(properties.getProperty("scanPackage"));
        //3. 初始化Bean对象（实现IOC容器，基于注解）
        doInstance();
        //4. 实现依赖注入
        doAutoWired();
        //5. 构造一个handleMapping处理器映射器，将配置好的url和method建立映射关系
        initMethodHandleMapping();
        System.out.println("MVC 初始化完成");
    }


    /**
     * 1，加载配置文件
     *
     * @param contextConfigLocation
     */
    private void doLoadConfig(String contextConfigLocation) {
        //根据指定路径加载成流：
        InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
        try {
            properties.load(resourceAsStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 2,扫描类
     * scanPackage :com.alibaba.demo    package--->磁盘的文件夹（File）
     *
     * @param scanPackage
     */
    private void doScan(String scanPackage) {
        //1.获得classPath路径
        String clasPath = Thread.currentThread().getContextClassLoader().getResource("").getPath();
        //2.拼接,得到scanPackage在磁盘上的路径
        String scanPackagePath = clasPath + scanPackage.replaceAll("\\.", "/");
        File pack = new File(scanPackagePath);
        File[] files = pack.listFiles();
        for (File file : files) {
            //子 package
            if (file.isDirectory()) {
                //递归
                doScan(scanPackage + "." + file.getName());  //com.alibaba.controller.controller
            } else if (file.getName().endsWith(".class")) {
                String className = scanPackage + "." + file.getName().replace(".class", "");
                classNames.add(className);
            }
        }
    }

    /**
     * 3,IOC容器
     * 基于classNames缓存的类的全限定类名，以及反射技术，完成对象创建和管理
     * 3.1遍历List，依次得到所有的全类名
     * 3.2通过反射得到类对象
     * 3.3根据类对象判断有没有注解，并区分controller和servicecontroller，
     * 它的id此处不做过多处理，不取value了，用类的首字母小写作为id，保存到IOC容器中。
     * service，service层往往是有接口的，再以接口名为id再存入一份bean到ioc，便于后期根据接口类型注入
     */
    private void doInstance() {
        if (classNames.size() == 0) {
            return;
        }
        try {
            for (int i = 0; i < classNames.size(); i++) {
                String className = classNames.get(i);  //com.alibaba.controller.DemoController
                //反射
                Class<?> aClass = Class.forName(className);
                //区分controller ，区分service
                if (aClass.isAnnotationPresent(Controller.class)) {
                    //controller的id此处不做过多处理，不取value了，用类的首字母小写作为id，保存到IOC容器中
                    String simpleName = aClass.getSimpleName();//DemoController
                    //demoController
                    String lowerFirstSimpleName = simpleName.substring(0, 1).toLowerCase() + simpleName.substring(1);
                    Object bean = aClass.newInstance();
                    ioc.put(lowerFirstSimpleName, bean);
                } else if (aClass.isAnnotationPresent(Service.class)) {
                    Service annotation = aClass.getAnnotation(Service.class);
                    //获取注解的值
                    String beanName = annotation.value();
                    //指定了id就以指定的id为准
                    if (!"".equals(beanName.trim())) {
                        ioc.put(beanName, aClass.newInstance());
                    } else {
                        //没有指定id ，首字母小写
                        String lowerFirstSimpleName =
                                aClass.getSimpleName().substring(0, 1).toLowerCase() +
                                        aClass.getSimpleName().substring(1);
                        ioc.put(lowerFirstSimpleName, aClass.newInstance());
                    }
                    //service层往往是有接口的，再以接口名为id再存入一分bean到ioc，便于后期根据接口类型注入
                    Class<?>[] interfaces = aClass.getInterfaces();
                    for (Class<?> anInterface : interfaces) {
                        //以接口的类名作为id放入。
                        ioc.put(anInterface.getName(), aClass.newInstance());
                    }
                } else {
                    //TODO do nothing
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 4,实现依赖注入
     * 4.1遍历ioc这个map，得到每个对象
     * 4.2获取对象的字段（属性）信息
     * 4.3判断字段是否被@Autowired修饰
     * 4.4判断@Autowired有没有设置value值有，直接从ioc容器中取出，然后设置属性。无，需要根据当前字段的类型注入（接口注入）
     */
    private void doAutoWired() {
        if (ioc.isEmpty()) {
            return;
        }
        //1，判断容器中有没有被@Autowried注解的属性，如果有需要维护依赖注入关系
        for (Map.Entry<String, Object> entry : ioc.entrySet()) {
            //获取bean对象中的字段信息
            Field[] declaredFields = entry.getValue().getClass().getDeclaredFields();
            for (Field declaredField : declaredFields) {
                if (!declaredField.isAnnotationPresent(Autowired.class)) {
                    continue;
                }
                //有该注解：
                Autowired annotation = declaredField.getAnnotation(Autowired.class);
                String beanName = annotation.value(); //需要注入的bean的Id
                if ("".equals(beanName.trim())) {
                    //没有配置具体的beanId，需要根据当前字段的类型注入（接口注入）  IDemoService
                    beanName = declaredField.getType().getName();
                }
                //开启赋值
                declaredField.setAccessible(true);
                try {
                    //字段调用，两个参数：(哪个对象的字段，传入什么)
                    declaredField.set(entry.getValue(), ioc.get(beanName));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 5，构造一个映射器,将url和{@link MethodHandler}进行关联
     * 之前我们所有被@Controller（自定义Controller注解）的类，都存在了ioc 这个map中。
     * 我们可以遍历这个map，得到每个bean对象
     * 然后判断是否被@Controller所修饰（排除@Service所修饰的bean）
     * 然后判断是否被@RequestMapping所修饰，有的话，就取其value值，作为baseUrl
     * 然后遍历该bean对象中的所有方法，得到被@RequestMapping修饰的方法。得到其value值，作为methodUrl。
     * baseUrl + methodUrl = url
     * 我们把url和{@link MethodHandler}进行关联。
     * MethodHandler包含 object（需要调用的对象）和当前method（method params 的type 和args）。
     */
    private void initMethodHandleMapping() {
        if (ioc.isEmpty()) {
            return;
        }
        for (Map.Entry<String, Object> entry : ioc.entrySet()) {
            //获取ioc中当前遍历对象的class类型
            Class<?> clazz = entry.getValue().getClass();
            //排除非controller层的类
            if (!clazz.isAnnotationPresent(Controller.class)) {
                continue;
            }
            String baseUrl = "";
            if (clazz.isAnnotationPresent(RequestMapping.class)) {
                //Controller层 类上 注解@RequestMapping中的value值
                baseUrl = clazz.getAnnotation(RequestMapping.class).value();
            }
            //获取方法
            Method[] methods = clazz.getMethods();
            for (Method method : methods) {
                //排除没有@RequestMapping注解的方法
                if (!method.isAnnotationPresent(RequestMapping.class)) {
                    continue;
                }
                //Controller层 类中方法上 注解@RequestMapping中的value值
                String methodUrl = method.getAnnotation(RequestMapping.class).value();
                String url = baseUrl + methodUrl;
                //把method所有信息以及url封装为MethodHandler
                MethodHandler methodHandler = new MethodHandler(entry.getValue(), method, url);
                //处理计算方法的参数位置信息
                Parameter[] parameters = method.getParameters();
                for (int i = 0; i < parameters.length; i++) {
                    Parameter parameter = parameters[i];
                    //不做太多的参数类型判断，只做：HttpServletRequest request, HttpServletResponse response和基本类型参数
                    if (parameter.getType() == HttpServletRequest.class || parameter.getType() == HttpServletResponse.class) {
                        //如果时request和response对象，那么参数名称存 HttpServletRequest 和 HttpServletResponse
                        methodHandler.getParamIndexMapping().put(parameter.getType().getSimpleName(), i);
                    } else {
                        methodHandler.getParamIndexMapping().put(parameter.getName(), i);
                    }
                }
                methodHandlerMap.put(url, methodHandler);
            }
        }
    }


    /**
     * 6, 请求处理开发 doPost()：
     * 上一步骤，我们配置了 uri和method的映射关系，并封装到了Handler中存入list，
     * 那么接下来，就要通过HttpServletRequest，取出uri，然后找到具体的Handler：
     * <p>
     * 6.1 通过HttpServletRequest取出uri找到具体的Handler
     * 6.2 得到将调用方法的参数的数组
     * 6.3 根据上述数组长度创建一个新的数组（参数数组，传入反射调用的）
     * 6.4 通过req.getParameterMap()得到前台传来的参数parameterMap
     * 6.5 遍历parameterMap
     * 6.6 通过StringUtils.join方法把name=1&name=2格式的参数变为name[1,2] （需要commons-lang依赖）
     * 6.7 参数匹配并设置
     */
    @SuppressWarnings("unchecked")
    private Object invokeMethod(HttpServletRequest req, HttpServletResponse resp) {
        //6.1根据uri获取到能够处理当前请求的MethodHandler
        MethodHandler handler = methodHandlerMap.get(req.getRequestURI());
        if (handler == null) {
            return "404 not found";
        }
        /**
         * 参数绑定
         */
        //6.2该方法所有参数得类型数组
        Class<?>[] parameterTypes = handler.getMethod().getParameterTypes();
        //6.3根据上述数组长度创建一个新的数组（参数数组，传入反射调用的）
        Object[] paramValues = new Object[parameterTypes.length];
        //6.4以下就是为了向参数数组中设值，而且还得保证参数得顺序和方法中形参顺序一致。
        //TODO 这里的key是params，value是 一个数组，数组中填充多个val。 因为url中的params可以重复
        Map<String, String[]> parameterMap = req.getParameterMap();
        //6.5遍历request中所有的参数 ，（填充除了request、response之外的参数）
        for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
//            //name=1&name=2  --> name[1,2]
//           //6.6 通过StringUtils.join方法把name[1,2]格式的参数变为 1,2 （需要commons-lang依赖）
            String value = StringUtils.join(entry.getValue(), ",");// 如同 1,2
            //如果参数和方法中的参数匹配上了，填充数据
            if (!handler.getParamIndexMapping().containsKey(entry.getKey())) {
                continue;
            }
            //6.7 方法形参确实有该参数，找到它得索引位置，对应得把参数值放入paramValues
            Integer index = handler.getParamIndexMapping().get(entry.getKey());
            //把前台传递过来的参数值，填充到对应得位置去
            paramValues[index] = value;

        }
        //填充req、resp参数，如果有
        if (handler.getParamIndexMapping().containsKey(HttpServletRequest.class.getSimpleName())) {
            Integer requestIndex = handler.getParamIndexMapping().get(HttpServletRequest.class.getSimpleName());
            paramValues[requestIndex] = req;
        }
        if (handler.getParamIndexMapping().containsKey(HttpServletResponse.class.getSimpleName())) {
            Integer requestIndex = handler.getParamIndexMapping().get(HttpServletResponse.class.getSimpleName());
            paramValues[requestIndex] = resp;
        }
        //最终调用handler得method属性
        try {
            return handler.getMethod().invoke(handler.getController(), paramValues);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}